package com.omt.tarjimdinek.conversation;

import static com.google.common.collect.Maps.newHashMap;
import static com.omt.tarjimdinek.conversation.ConversationHolder.setCurrentConversation;

import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;

import javax.faces.context.FacesContext;
import javax.inject.Named;
import javax.inject.Singleton;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.primefaces.component.menuitem.MenuItem;
import org.primefaces.model.DefaultMenuModel;
import org.primefaces.model.MenuModel;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

/**
 * The conversation manager is responsible for creating conversations, managing their lifecycle and calling the conversation listeners.
 */
@Named
@Singleton
public class ConversationManager implements ApplicationContextAware {
    private static final String CONVERSATION_MAP = "conversationMap";
    private static ConversationManager instance;
    private ApplicationContext applicationContext;
    private Collection<ConversationFactory> conversationFactories;
    private Collection<ConversationListener> conversationListeners;
    private Map<String, ConversationFactory> conversationFactoryByUri = newHashMap();
    private int maxConversations = 5;

    /**
     * This method should be used only in the following cases: 1) from code having no spring awareness, like filters. 2) from code that are session scoped in
     * order to avoid serialization of the service. In other cases, please have the conversationManager injected normally.
     */
    static public ConversationManager getInstance() {
        return instance;
    }

    public ConversationManager() {
        if (instance == null) {
            instance = this;
        }
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * The maximum number of conversations a given user can open simultaneously.
     */
    public int getMaxConversations() {
        return maxConversations;
    }

    /**
     * Whether the max number of conversations per user is reached. Used in from the ConversationFilter (which has no FacesContext yet).
     */
    public boolean isMaxConversationsReached(HttpSession session) {
        return conversationMap(session).size() >= maxConversations;
    }

    /**
     * Returns the current conversation. Note that this method is mainly here so it can be used from the view.
     * Use directly ConversationHolder.getCurrentConversation() from Java code.
     */
    public Conversation getCurrentConversation() {
        return ConversationHolder.getCurrentConversation();
    }

    // --------------------------------------
    // Manage conversation lifecycle
    // --------------------------------------

    /**
     * Creates a new {@link Conversation}, calls the {@link ConversationListener#conversationCreated} but does NOT bound the newly created conversation to
     * the current thread.
     */
    public Conversation createConversation(HttpServletRequest request) throws UnexpectedConversationException {
        ConversationFactory conversationFactory = getConversationFactory(request);
        if (conversationFactory == null) {
            throw new UnexpectedConversationException("No conversation factory found", request.getRequestURI(), "/home.faces");
        }
        Conversation conversation = conversationFactory.createConversation(request);
        conversationMap(request.getSession()).put(conversation.getId(), conversation);
        conversationCreated(conversation);
        return conversation;
    }

    /**
     * Resume the {@link Conversation} having the passed id. Before resuming it, if a pending ConversationContext is present, 
     * it is pushed on the conversation contextes stack. 
     * @param id the id of the conversation to resume 
     * @param request
     * @throws UnexpectedConversationException
     */
    public void resumeConversation(String id, HttpServletRequest request) throws UnexpectedConversationException {
        Conversation conversation = conversationMap(request.getSession()).get(id);

        if (conversation != null) {
            conversation.pushNextContextIfNeeded();

            if (!request.getRequestURI().contains(conversation.getViewUri())) {
                throw new UnexpectedConversationException("Uri not in sync with conversation", request.getRequestURI(), conversation.getUrl());
            }
            conversationResuming(conversation, request);
            setCurrentConversation(conversation);
        } else {
            throw new UnexpectedConversationException("No conversation found for id=" + id, request.getRequestURI(), "/home.faces");
        }
    }

    /**
     * Pause the current conversation. Before pausing it, pops the current context as needed.  
     */
    public void pauseCurrentConversation() {
        Conversation conversation = getCurrentConversation();

        // we check for not null because the conversation could have 
        // been ended during the current request.
        if (conversation != null) {
            // call order of 2 methods below is important as we want all the contextes (even the one we are about to be popped)
            // to be visible from the conversation listener.
            conversationPausing(conversation);
            conversation.popContextesIfNeeded();
            setCurrentConversation(null);
        }
    }

    /**
     * End the current Conversation.
     */
    public void endCurrentConversation() {
        Conversation conversation = getCurrentConversation();
        conversationEnding(conversation);
        setCurrentConversation(null);
        conversationMap().remove(conversation.getId());
    }

    // --------------------------------------------
    // Used from view to display conversations menu
    // --------------------------------------------

    /**
     * Returns the number of conversations for the current user. Used in the conversation menu.
     */
    public int getConversationCount() {
        return conversationMap().size();
    }

    public MenuModel getConversationMenuModel() {
        MenuModel model = new DefaultMenuModel();
        Conversation currentConversation = getCurrentConversation();
        for (Conversation conversation : conversationMap().values()) {
            MenuItem htmlMenuItem = new MenuItem();
            htmlMenuItem.setValue(conversation.getLabel());
            htmlMenuItem.setUrl(conversation.getUrl());
            if (currentConversation != null && currentConversation.getId().equals(conversation.getId())) {
                htmlMenuItem.setDisabled(true);
            }
            model.addMenuItem(htmlMenuItem);
        }
        return model;
    }

    public boolean getRenderBreadCrumb() {
        return getCurrentConversation().getConversationContextes().size() > 1;
    }

    public MenuModel getBreadCrumbMenuModel() {
        MenuModel model = new DefaultMenuModel();
        Conversation currentConversation = getCurrentConversation();
        Stack<ConversationContext<?>> ctxStack = currentConversation.getConversationContextes();
        int beforeLastIndex = ctxStack.size() - 2;
        Iterator<ConversationContext<?>> iterator = ctxStack.iterator();

        // first item is rendered as ui-icon-home => we don't want it so we disable it.
        MenuItem menuItem = new MenuItem();
        menuItem.setRendered(false);
        model.addMenuItem(menuItem);

        int i = 0;
        while (iterator.hasNext()) {
            ConversationContext<?> ctx = iterator.next();
            menuItem = new MenuItem();
            menuItem.setValue(ctx.getLabel());
            if (i == beforeLastIndex && beforeLastIndex > 0) {
                // calls back button action
                menuItem.setOnclick("APP.menu.back()");
            } else {
                menuItem.setDisabled(true);
            }

            model.addMenuItem(menuItem);
            i++;
        }
        return model;
    }

    // --------------------------------------------
    // Impl details
    // --------------------------------------------    

    private Map<String, Conversation> conversationMap() {
        @SuppressWarnings("unchecked")
        Map<String, Conversation> map = (Map<String, Conversation>) sessionMap().get(CONVERSATION_MAP);
        if (map == null) {
            map = newHashMap();
            sessionMap().put(CONVERSATION_MAP, map);
        }
        return map;
    }

    private Map<String, Conversation> conversationMap(HttpSession session) {
        @SuppressWarnings("unchecked")
        Map<String, Conversation> map = (Map<String, Conversation>) session.getAttribute(CONVERSATION_MAP);
        if (map == null) {
            map = newHashMap();
            session.setAttribute(CONVERSATION_MAP, map);
        }
        return map;
    }

    private Map<String, Object> sessionMap() {
        return FacesContext.getCurrentInstance().getExternalContext().getSessionMap();
    }

    private Collection<ConversationFactory> getConversationFactories() {
        if (conversationFactories == null) {
            conversationFactories = applicationContext.getBeansOfType(ConversationFactory.class).values();
        }
        return conversationFactories;
    }

    private ConversationFactory getConversationFactory(HttpServletRequest request) {
        String uri = request.getServletPath();
        ConversationFactory result = conversationFactoryByUri.get(uri);

        if (result == null) {
            for (ConversationFactory cf : getConversationFactories()) {
                if (cf.canCreateConversation(request)) {
                    conversationFactoryByUri.put(uri, cf);
                    result = cf;
                    break;
                }
            }
        }

        return result;
    }

    // --------------------------------------------
    // Support for conversation listeners
    // --------------------------------------------    

    private Collection<ConversationListener> getConversationListeners() {
        if (conversationListeners == null) {
            conversationListeners = applicationContext.getBeansOfType(ConversationListener.class).values();

        }
        return conversationListeners;
    }

    private void conversationCreated(Conversation conversation) {
        for (ConversationListener cl : getConversationListeners()) {
            cl.conversationCreated(conversation);
        }
    }

    private void conversationPausing(Conversation conversation) {
        for (ConversationListener cl : getConversationListeners()) {
            cl.conversationPausing(conversation);
        }
    }

    private void conversationResuming(Conversation conversation, HttpServletRequest request) {
        for (ConversationListener cl : getConversationListeners()) {
            cl.conversationResuming(conversation, request);
        }
    }

    private void conversationEnding(Conversation conversation) {
        for (ConversationListener cl : getConversationListeners()) {
            cl.conversationEnding(conversation);
        }
    }
}
